﻿using System;
using System.Collections.Generic;
using System.Xml;
using System.Xml.XPath;

namespace InfoPathHelpers.RepeatingTable.Paginator
{
    public class RepeatingTablePaginator
    {
        const string DEFAULT_XPATH              = "my:Main/my:PrintView";
        const string DEFAULT_PAGE_NAME          = "Page";
        const string DEFAULT_TABLE_NAME         = "Table";
        const string DEFAULT_ROW_NAME           = "Row";
        const string DEFAULT_COLUMN_PREFIX      = "Column";
        const double DEFAULT_LINE_MULTIPLIER    = 0.5;
        // The default padding for repeating table columns.
        const int DEFAULT_PADDING = 10;

        public RepeatingTablePaginator()
        {
            SetDefaults();
            TextMeasurer = new GraphicsTextMeasurer();
        }

        public RepeatingTablePaginator(ITextMeasurer textMeasurer)
        {
            SetDefaults();
            TextMeasurer = textMeasurer;
        }

        private void SetDefaults()
        {
            PrintViewXPath = DEFAULT_XPATH;
            PageLocalName = DEFAULT_PAGE_NAME;
            TableLocalName = DEFAULT_TABLE_NAME;
            RowLocalName = DEFAULT_ROW_NAME;
            ColumnPrefix = DEFAULT_COLUMN_PREFIX;
            LineMultiplier = DEFAULT_LINE_MULTIPLIER;
            SourceTableDescription = new RepeatingTableDescription();
        }

        /// <summary>
        /// The XPath to the group containing Pages.
        /// Default: my:Main/my:PrintView
        /// </summary>
        public string PrintViewXPath { get; set; }

        /// <summary>
        /// The local name of the repeating Page group.
        /// Default: Page
        /// </summary>
        public string PageLocalName { get; set; }

        /// <summary>
        /// The local name of the group containing repeating table rows.
        /// Default: Table
        /// </summary>
        public string TableLocalName { get; set; }

        /// <summary>
        /// The local name of the repeating Row group.
        /// Default: Row
        /// </summary>
        public string RowLocalName { get; set; }

        public RepeatingTableDescription SourceTableDescription { get; set; }

        /// <summary>
        /// The default prefix for columns in the destination table. 
        /// All columns should be labelled [Prefix]n.
        /// Default: Column
        /// </summary>
        public string ColumnPrefix { get; set; }

        /// <summary>
        /// The maximum number of lines allowed per page for this table.
        /// </summary>
        public int MaxLinesPerPage { get; set; }

        /// <summary>
        /// A text box with 2 lines does not require the same space as 2 full rows.
        /// This multiplier is used to adjust for multi line text boxes. And should
        /// be adjusted if not using default line spacing options. 
        /// The first line is 1 and each subsequent line is 1n where n is the line multiplier.
        /// Default: (0.5) 50%
        /// </summary>
        public double LineMultiplier { get; set; }

        private string RowXPath
        {
            get
            {
                return string.Format("{0}/my:{1}/my:{2}/my:{3}", 
                    PrintViewXPath, PageLocalName, TableLocalName, RowLocalName);
            }
        }

        private string TableXPath
        {
            get
            {
                return string.Format("{0}/my:{1}/my:{2}",
                    PrintViewXPath, PageLocalName, TableLocalName);
            }
        }

        private string PageXPath
        {
            get
            {
                return string.Format("{0}/my:{1}",
                    PrintViewXPath, PageLocalName);
            }
        }

        private string Namespace { get; set; }

        private ITextMeasurer TextMeasurer { get; set; }

        private IXmlNamespaceResolver NamespaceManager { get; set; }

        private List<RepeatingTableColumnDescription> MultiLineColumns
        {
            get
            {
                return SourceTableDescription
                        .Columns
                        .FindAll(x => x.IsMultiLine == true);
            }
        }

        public void GeneratePrintView(FormContext formContext)
        {
            Namespace = formContext.NamespacePrefix + ":";
            NamespaceManager = formContext.NamespaceManager;

            var CurrentLineCount = 0d;
            var CurrentPageIndex = 1;

            var SourceTable = formContext.Select(SourceTableDescription.XPath);

            // Save a copy of the first row to use as a template.
            var SampleRow = formContext.SelectSingleNode(RowXPath);
            var FirstRow = formContext.SelectSingleNode(RowXPath);
            var LastRow = formContext.SelectSingleNode(RowXPath + "[last()]");
            // Delete all Rows.
            FirstRow.DeleteRange(LastRow);

            var SampleTable = formContext.SelectSingleNode(TableXPath);
            var Table = formContext.SelectSingleNode(TableXPath);
            Table.DeleteSelf();

            var SamplePage = formContext.SelectSingleNode(PageXPath);
            var FirstPage = formContext.SelectSingleNode(PageXPath);
            var LastPage = formContext.SelectSingleNode(PageXPath + "[last()]");
            FirstPage.DeleteRange(LastPage);

            var Document = formContext.SelectSingleNode(PrintViewXPath);

            Document.AppendChild(SamplePage.Clone());

            var NewPage = formContext.SelectSingleNode(PageXPath);

            NewPage.AppendChild(SampleTable.Clone());

            while (SourceTable.MoveNext())
            {
                var SourceRow = SourceTable.Current;

                var LinesInRow = CountLinesInRow(SourceRow);
                CurrentLineCount += LinesInRow;

                if (CurrentLineCount > MaxLinesPerPage && LinesInRow <= MaxLinesPerPage)
                {
                    // Break onto a new page if the current row will be broken.
                    // Unless the row is bigger than a full page in which case it has to be broken anyway.
                    CurrentLineCount = LinesInRow;
                    CurrentPageIndex += 1;
                    Document.AppendChild(SamplePage.Clone());
                    NewPage = formContext.SelectSingleNode(PageXPath + "[" + CurrentPageIndex + "]");
                    NewPage.AppendChild(SampleTable.Clone());
                }

                var NewRow = SampleRow.Clone();
                for (int i = 0; i < SourceTableDescription.Columns.Count; i++)
                {
                    NewRow.SelectSingleNode(Namespace + ColumnPrefix + (i + 1), NamespaceManager)
                        .SetValue(SourceRow
                            .SelectSingleNode(Namespace + SourceTableDescription.Columns[i].LocalName, NamespaceManager)
                            .Value);
                }

                NewPage.SelectSingleNode(Namespace + TableLocalName, NamespaceManager).AppendChild(NewRow);
            }
        }

        /// <summary>
        /// Estimates the number of lines in a repeating table row, given the font used and the width of the column.
        /// Note: Assumes default formatting. Widths must be adjusted if using alternate formats for columns. 
        /// </summary>
        private double CountLinesInRow(XPathNavigator tableRow)
        {
            var RowLineCount = 0d;

            foreach (var Column in MultiLineColumns)
            {
                var ColumnText = tableRow.SelectSingleNode(Namespace + Column.LocalName, NamespaceManager).Value.Split('\n');
                var ColumnLineCount = CountLinesInColumn(Column, ColumnText);

                // We compare the number of lines in this column to the last measured to always
                // get the maximum number of lines in the row.
                RowLineCount = Math.Max(ColumnLineCount, RowLineCount);
            }

            return RowLineCount;
        }

        private double CountLinesInColumn(RepeatingTableColumnDescription Column, string[] columnText)
        {
            TextMeasurer.Font = Column.Font;
            var ColumnLineCount = 0d;

            if (Column.IsMultiLine)
            {
                // If a column is marked as being multi line then we need to count the lines.
                foreach (var Line in columnText)
                {
                    if (Line.Length != 0)
                    {
                        // If the line has contents we need to measure the width in case it wraps.
                        var LineWidth = TextMeasurer.Measure(Line);
                        // Calculate the number of lines that the text will wrap over base on column width.
                        var NumberOfLines = Math.Ceiling(LineWidth / ((Column.Width - Column.Padding) * Column.WidthAdjust));
                        ColumnLineCount += NumberOfLines;
                    }
                    else
                    {
                        // If the line has no length we just add a single line.
                        ColumnLineCount++;
                    }
                }
            }
            else
            {
                // The column is only a single line.
                ColumnLineCount++;
            }

            return 1 + ((ColumnLineCount - 1) * LineMultiplier);
        }
    }
}
